Unlock silky-smooth, performant scroll-based animations with pure CSS. This guide covers animation-timeline and animation-range for precise control.
CSS Animation Range: A Deep Dive into Scroll-Driven Animation Control
For years, creating animations that react to a user's scroll position has been a cornerstone of engaging web experiences. From subtle fade-ins to complex parallax effects, these interactions breathe life into static pages. However, they have traditionally come with a significant cost: reliance on JavaScript. Libraries and custom scripts that listen to scroll events can be performance-intensive, running on the main thread and potentially leading to janky, unresponsive user experiences, especially on less powerful devices.
Enter a new era of web animation. The latest advancements in CSS are revolutionizing how we handle these interactions. The Scroll-Driven Animations specification provides a powerful, declarative, and highly performant way to link animations directly to a scrollbar's position or an element's visibility within the viewport—all without a single line of JavaScript.
At the heart of this new paradigm are two key properties: animation-timeline and animation-range. While animation-timeline sets the stage by defining what drives the animation (e.g., the document's scrollbar), it's animation-range that gives us the granular control we've always craved. It allows us to define the precise start and end points of an animation within that timeline.
In this comprehensive guide, we will explore the world of CSS Scroll-Driven Animations with a special focus on animation-range. We will cover:
- The fundamental concepts of Scroll and View Progress Timelines.
- A detailed breakdown of the
animation-rangeproperty and its syntax. - Practical, real-world examples for creating progress bars, reveal effects, and more.
- Best practices for performance, accessibility, and browser compatibility.
Get ready to unlock animations that are not only beautiful but also incredibly efficient, moving complex logic from the main thread to the compositor thread for a guaranteed silky-smooth ride.
Understanding the Foundations: What are Scroll-Driven Animations?
Before we dive into animation-range, it's crucial to understand the system it operates within. Traditionally, CSS animations are tied to a time-based timeline. When you define animation-duration: 3s;, the animation progresses from 0% to 100% over three seconds, driven by a clock.
Scroll-Driven Animations fundamentally change this. They introduce the concept of a Progress Timeline, which is driven not by time, but by progress—either the progress of scrolling a container or the progress of an element's visibility as it moves through the viewport.
This new model offers three major advantages:
- Performance: Because these animations can be run off the main thread on the browser's compositor thread, they don't compete for resources with JavaScript, layout, or paint operations. The result is exceptionally smooth animation, free from the jank that often plagues JS-based scroll listeners.
- Simplicity: The CSS syntax is declarative. You state what you want to happen, and the browser handles the complex calculations. This often leads to cleaner, more maintainable code compared to imperative JavaScript.
- Accessibility: The animations respect user preferences like
prefers-reduced-motionout of the box, making it easier to build inclusive experiences.
There are two primary types of progress timelines you'll work with:
- Scroll Progress Timeline: Tracks the scroll position within a scroll container (a "scroller"). The timeline represents the entire scrollable range, from the very top (0%) to the very bottom (100%).
- View Progress Timeline: Tracks an element's visibility as it crosses the viewport. The timeline represents the element's journey from just entering the viewport to completely exiting it.
The Core Concept: The `animation-timeline` Property
The first step in creating a scroll-driven animation is to detach a standard CSS animation from its default time-based clock and attach it to a new progress-based timeline. This is done using the animation-timeline property.
It accepts a function that defines the source of the timeline: either scroll() for a Scroll Progress Timeline or view() for a View Progress Timeline.
Scroll Progress Timeline: `scroll()`
The scroll() function ties an animation to the scroll position of a container. Its most common form is scroll(scroller, axis).
scroller: Specifies which scrolling element to track. It can beroot(the document's viewport),self(the element itself, if it's a scroller), ornearest(the closest ancestor scroller).axis: Specifies the scroll axis to track. It can beblock(the primary direction of content flow, usually vertical),inline(perpendicular to block, usually horizontal),y, orx.
Example: A Simple Document Scroll Progress Bar
Let's create a progress bar at the top of the page that grows as the user scrolls down.
<!-- HTML -->
<div id="progress-bar"></div>
<!-- CSS -->
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
#progress-bar {
position: fixed;
top: 0;
left: 0;
height: 10px;
width: 100%;
background-color: dodgerblue;
transform-origin: left;
/* Detach from time, attach to document scroll */
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
In this example, the grow-progress animation is now driven by scrolling the main document (root) on its vertical axis (block). As you scroll from 0% to 100% of the page, the progress bar's scaleX transform goes from 0 to 1.
View Progress Timeline: `view()`
The view() function ties an animation to an element's visibility within its scroller. This is incredibly useful for triggering "reveal" animations as elements come into view.
Its syntax is view(axis, inset).
axis: Optional, same values as inscroll()(e.g.,block). Defines which axis of the scrollport to consider.inset: Optional, allows you to adjust the boundaries of the viewport used for calculating visibility, similar toIntersectionObserver'srootMargin.
Example: Fading In an Element
<!-- HTML -->
<div class="content-box reveal">
This box will fade in as it enters the screen.
</div>
<!-- CSS -->
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.reveal {
/* Attach animation to this element's visibility */
animation: fade-in linear;
animation-timeline: view();
}
Here, the fade-in animation is linked to the .reveal element itself. The animation will progress as the element travels across the viewport. But how exactly does it map? When does it start and end? That's where animation-range comes in.
The Star of the Show: `animation-range`
While animation-timeline sets the context, animation-range provides the critical control. It defines which part of the timeline is considered "active" and maps it to the 0% to 100% progress of your @keyframes animation.
The basic syntax is:
animation-range: <range-start> <range-end>;
This tells the browser: "When the timeline reaches the <range-start> point, the animation should be at 0%. When it reaches the <range-end> point, the animation should be at 100%."
The values for <range-start> and <range-end> can be one of several types:
- Keywords (for
view()): Special, highly intuitive names likeentry,exit,cover, andcontain. We'll explore these in detail. - Percentages: A percentage of the total timeline duration. For a
scroll()timeline,0%is the top and100%is the bottom. - CSS Lengths: A fixed length value like
100pxor20rem. This specifies a point at that scroll offset from the beginning of the timeline.
You can even combine keywords with percentages or lengths for extremely fine-grained control, like entry 50% or cover 200px.
Practical Deep Dive: `animation-range` with `scroll()` Timelines
When working with a scroll() timeline, you are mapping your animation to the scroller's overall scroll range. Let's see how animation-range helps us target specific parts of that journey.
Targeting a Specific Scroll Section
Imagine you have a long article and you want a specific graphic to animate only while the user is scrolling through the middle half of the page.
@keyframes spin-and-grow {
from { transform: rotate(0deg) scale(0.5); opacity: 0; }
to { transform: rotate(360deg) scale(1); opacity: 1; }
}
.special-graphic {
animation: spin-and-grow linear;
animation-timeline: scroll(root block);
/* Animation starts at 25% scroll and ends at 75% scroll */
animation-range: 25% 75%;
}
How it works:
- Before the user has scrolled 25% of the page, the animation is held at its 0% state (
rotate(0deg) scale(0.5) opacity: 0). - As the user scrolls from the 25% mark to the 75% mark, the animation progresses from 0% to 100%.
- After the user scrolls past the 75% mark, the animation is held at its 100% state (
rotate(360deg) scale(1) opacity: 1).
This simple addition of animation-range gives us powerful control over the timing and placement of our effects within the larger scroll experience.
Using Absolute Lengths
You can also use absolute lengths. For instance, if you want an animation to happen only over the first 500 pixels of scrolling:
.hero-animation {
animation: fade-out linear;
animation-timeline: scroll(root block);
/* Animation starts at scroll offset 0px and ends at 500px */
animation-range: 0px 500px;
}
This is perfect for introductory animations in a page's hero section that should conclude once the user has started to scroll deeper into the content.
Mastering `animation-range` with `view()` Timelines
This is where animation-range becomes truly magical. When used with a view() timeline, the range values are not based on the entire document's scroll height, but on the element's visibility within the viewport. This is where the special named ranges come into play.
The Named Ranges Explained
Imagine an element (the "subject") and the viewport (the "scroller"). The named ranges describe the relationship between these two boxes.
entry: The phase where the subject is entering the viewport. It begins the moment the subject's bottom edge crosses the viewport's top edge and ends when the subject's bottom edge crosses the viewport's bottom edge.exit: The phase where the subject is leaving the viewport. It begins when the subject's top edge crosses the viewport's top edge and ends when the subject's top edge crosses the viewport's bottom edge.cover: The phase where the subject is large enough to completely cover the viewport. It begins when the subject's top edge hits the viewport's top edge and ends when the subject's bottom edge hits the viewport's bottom edge. If the subject is smaller than the viewport, this phase never occurs.contain: The phase where the subject is fully contained within the viewport. It begins when the subject's bottom edge enters the viewport's bottom edge and ends when the subject's top edge exits the viewport's top edge. If the subject is larger than the viewport, this phase never occurs.
Practical Example: The Classic "Reveal on Scroll" Effect
Let's recreate one of the most common scroll-based animations: an element that fades and slides into view as it enters the screen. Traditionally, this required an Intersection Observer in JavaScript. Now, it's a few lines of CSS.
<!-- HTML -->
<section>
<div class="content-box reveal">Box 1</div>
<div class="content-box reveal">Box 2</div>
<div class="content-box reveal">Box 3</div>
</section>
<!-- CSS -->
@keyframes fade-and-slide-in {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
.reveal {
animation: fade-and-slide-in linear both; /* 'both' is important! */
animation-timeline: view();
/* Start animation when element enters, end when it's halfway through entering */
animation-range: entry 0% entry 50%;
}
Let's break down that animation-range value:
animation-fill-mode: both;is crucial. It ensures that before the animation's active range, the element stays at itsfromstate (invisible and shifted down), and after the range, it stays at itstostate (fully visible and in place).entry 0%: The start point. This refers to the very beginning of theentryphase—the exact moment the bottom of our element touches the bottom of the viewport.entry 50%: The end point. This refers to the moment the element has completed 50% of its journey through theentryphase. By this point, the animation will be 100% complete.
This gives a pleasant effect where the item is fully visible and in its final position well before the user has scrolled it to the center of the screen. You can tweak these percentages to get the exact feel you want. For example, entry 25% entry 75% would create a more drawn-out animation.
Advanced Control: Creating a Parallax Effect
Let's try a more complex effect. We'll make a background image move at a different speed than the scroll, but only while its container is covering the viewport.
<!-- HTML -->
<div class="parallax-container">
<div class="parallax-bg"></div>
<h2>Parallax Section</h2>
</div>
<!-- CSS -->
@keyframes parallax-shift {
from { background-position: 50% -50px; }
to { background-position: 50% 50px; }
}
.parallax-container {
position: relative;
height: 100vh;
overflow: hidden;
}
.parallax-bg {
position: absolute;
inset: -50px; /* Make it taller than container to allow movement */
background-image: url('your-image.jpg');
background-size: cover;
animation: parallax-shift linear both;
animation-timeline: view(block);
/* Animate across the entire 'cover' phase */
animation-range: cover 0% cover 100%;
}
In this case, the parallax-shift animation is tied to the parallax-bg element's timeline. The animation-range is set to the full duration of the cover phase. This means the animation starts progressing only when the container is tall enough to cover the viewport and is positioned such that its top is at the viewport's top. It finishes when the container's bottom reaches the viewport's bottom. The result is a smooth, performant parallax effect that is perfectly synchronized with the scroll position.
Combining It All: Shorthands and Best Practices
The `animation` Shorthand
To make the syntax even more concise, the timeline and range properties can be included directly in the animation shorthand property. This is a new, proposed syntax that is gaining support.
Our reveal-on-scroll example could be rewritten as:
.reveal {
animation: fade-and-slide-in linear both view() entry 0% entry 50%;
}
This single line replaces the three separate animation, animation-timeline, and animation-range properties. It's clean, efficient, and keeps all the animation logic in one place.
Performance Considerations
The primary benefit of scroll-driven animations is performance. To maintain this benefit, you should prioritize animating properties that can be handled by the compositor thread. These are primarily:
transform(translate, scale, rotate)opacity
Animating properties like width, height, margin, or color will still work, but they may trigger layout and paint operations, which happen on the main thread. While still often smoother than JS-based alternatives, they won't be as performant as compositor-only animations.
Accessibility and Fallbacks
It's crucial to build for all users. Scroll-driven animations are great, but some users find motion distracting or nauseating.
1. Respect User Preferences: Always wrap your motion-related CSS in a prefers-reduced-motion media query.
@media (prefers-reduced-motion: no-preference) {
.reveal {
animation: fade-and-slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
}
2. Provide Fallbacks for Older Browsers: Since this is a new technology, you must account for browsers that don't yet support it. The @supports rule is your best friend here. Provide a simple, non-animated default state, and then enhance it for supporting browsers.
/* Default state for all browsers */
.reveal {
opacity: 1;
transform: translateY(0);
}
/* Enhancement for supporting browsers */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.reveal {
opacity: 0; /* Set initial state for animation */
transform: translateY(50px);
animation: fade-and-slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
}
}
Browser Support and Looking Ahead
As of late 2023, CSS Scroll-Driven Animations are supported in Chrome and Edge. They are under active development in Firefox and being considered by Safari. As with any cutting-edge web platform feature, it's essential to check resources like CanIUse.com for the latest support information.
The introduction of this technology marks a significant shift in web development. It empowers designers and developers to create rich, interactive, and performant experiences declaratively, reducing our reliance on JavaScript for a whole class of common UI patterns. As browser support matures, expect to see scroll-driven animations become an essential tool in every front-end developer's toolkit.
Conclusion
CSS Scroll-Driven Animations, and specifically the animation-range property, represent a monumental leap forward for web animation. We've moved from time-based timelines to progress-based timelines, unlocking the ability to create complex, scroll-aware interactions with unparalleled performance and simplicity.
We've learned that:
animation-timelinelinks an animation to ascroll()orview()progress timeline.animation-rangegives us precise control, mapping a specific portion of that timeline to our animation's keyframes.- With
view()timelines, powerful named ranges likeentry,exit,cover, andcontainprovide an intuitive way to control animations based on an element's visibility. - By sticking to compositor-friendly properties and providing fallbacks, we can use this technology today to build accessible, performant, and delightful user experiences.
The days of fighting with janky, main-thread-blocking scroll listeners for simple effects are numbered. The future of scroll-based animation is here, it's declarative, and it's written in CSS. It's time to start experimenting.